這些只是目前測試用的簡單版本,後續會考慮到提示次數、解題時間等,都會列入計分。
保留前兩天backend/dto.py的內容,加入以下程式碼
# backend/dto.py
from typing import List
class AssessmentProblem(BaseModel):
    id: int
    slug: str
    title: str
    difficulty: str
    topic: str
    model_config = ConfigDict(from_attributes=True)
class AssessmentStartOut(BaseModel):
    user_id: int
    problems: List[AssessmentProblem]
class AssessmentItemIn(BaseModel):
    problem_id: int
    verdict: Literal["accepted", "wrong", "tle", "skipped"] = "skipped"
    hint_used: bool = False
class AssessmentScoreIn(BaseModel):
    user_id: int
    items: List[AssessmentItemIn]
class AssessmentResultOut(BaseModel):
    user_id: int
    level: Literal["beginner", "intermediate", "advanced"]
    score: float
    breakdown: dict
# backend/routers/assessment.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func
from ..database import SessionLocal
from .. import models
from ..dto import (
    AssessmentStartOut, AssessmentProblem,
    AssessmentScoreIn, AssessmentResultOut
)
router = APIRouter(prefix="/assessment", tags=["assessment"])
def get_db():
    db = SessionLocal()
    try: yield db
    finally: db.close()
def pick_one(db: Session, difficulty: str):
    # SQLite 的隨機排序
    return (db.query(models.Problem)
              .filter(models.Problem.is_active == True,
                      models.Problem.difficulty == difficulty)
              .order_by(func.random()).first())
@router.get("/start", response_model=AssessmentStartOut)
def start_assessment(user_id: int, db: Session = Depends(get_db)):
    user = db.query(models.User).filter(models.User.id == user_id,
                                        models.User.is_active == True).first()
    if not user:
        raise HTTPException(404, "user not found")
    e = pick_one(db, "Easy")
    m = pick_one(db, "Medium")
    h = pick_one(db, "Hard")
    problems = [p for p in [e, m, h] if p is not None]
    if not problems:
        raise HTTPException(400, "no problems available for assessment")
    out = AssessmentStartOut(
        user_id=user.id,
        problems=[AssessmentProblem.model_validate(p) for p in problems]
    )
    return out
@router.post("/score", response_model=AssessmentResultOut)
def score_assessment(payload: AssessmentScoreIn, db: Session = Depends(get_db)):
    user = db.query(models.User).filter(models.User.id == payload.user_id).first()
    if not user:
        raise HTTPException(404, "user not found")
    # 權重設定
    diff_w = {"Easy": 1.0, "Medium": 2.0, "Hard": 3.0}
    verdict_w = {"accepted": 1.0, "wrong": 0.2, "tle": 0.5, "skipped": 0.0}
    total = 0.0
    breakdown = {}
    for it in payload.items:
        p = db.query(models.Problem).get(it.problem_id)
        if not p:
            continue
        base = diff_w.get(p.difficulty, 1.0) * verdict_w.get(it.verdict, 0.0)
        if it.hint_used:
            base = max(0.0, base - 0.2)
        total += base
        breakdown[str(p.id)] = {
            "title": p.title,
            "difficulty": p.difficulty,
            "verdict": it.verdict,
            "hint_used": it.hint_used,
            "score": round(base, 2),
        }
    # 0~6 區間(最多 1+2+3=6)
    score = round(total, 2)
    if score > 4.0:
        level = "advanced"
    elif score >= 2.0:
        level = "intermediate"
    else:
        level = "beginner"
    # 寫回使用者等級
    user.level = level
    db.add(user); db.commit()
    return AssessmentResultOut(
        user_id=user.id,
        level=level,
        score=score,
        breakdown=breakdown
    )
把 assessment 路由註冊進 FastAPI,讓 assessment 可被存取。
在 app.py 裡 import 並 include
from .routers import assessment
...(原本的程式)
app.include_router(assessment.router)
做完以上步驟,就可以重啟後端:
python -m uvicorn backend.app:app --reload
Swagger 檢查:
GET /assessment/start?user_id=1 → 應回三題
POST /assessment/score:
{
  "user_id": 1, //編號記得改
  "items": [
    {"problem_id": 1, "verdict": "accepted", "hint_used": false},
    {"problem_id": 2, "verdict": "wrong", "hint_used": true},
    {"problem_id": 3, "verdict": "skipped", "hint_used": false}
  ]
}
→ 應回 level, score, breakdown 並更新 users.level
在現有的前端後面,加入一個三題快速分級的功能:
抽題 → 顯示題目 → 在畫面選擇 verdict/hint_used → 一鍵送出 → 顯示等級與每題計分。
將程式接續到frontend/app.py後面:
# frontend/app.py
st.header("程度測驗(3 題快速分級)")
if "assessment_user_id" not in st.session_state:
    st.session_state.assessment_user_id = 1   # 先固定 1,之後做登入再帶入
if "assessment_problems" not in st.session_state:
    st.session_state.assessment_problems = None
if "assessment_results" not in st.session_state:
    st.session_state.assessment_results = {}
col_a, col_b = st.columns([1, 3])
with col_a:
    if st.button("開始測驗 / 重新抽題"):
        try:
            resp = requests.get(
                f"{API_BASE}/assessment/start",
                params={"user_id": st.session_state.assessment_user_id},
                timeout=8
            )
            st.session_state.assessment_problems = resp.json()["problems"]
            st.session_state.assessment_results = {}  # reset
            st.success("已抽出 3 題,請回報結果")
        except Exception as e:
            st.error(f"抽題失敗:{e}")
if st.session_state.assessment_problems:
    st.write("請到 LeetCode 作答,或根據理解回報本題結果:")
    for p in st.session_state.assessment_problems:
        st.markdown(
            f"- **[{p['difficulty']}] {p['title']}** / topic: {p['topic']} / "
            f"slug: `{p['slug']}`"
        )
        verdict = st.selectbox(
            f"結果(題目 #{p['id']})",
            ["accepted", "wrong", "tle", "skipped"],
            key=f"verdict_{p['id']}"
        )
        hint_used = st.checkbox(
            f"該題有使用提示(題目 #{p['id']})",
            key=f"hint_{p['id']}"
        )
        st.session_state.assessment_results[p["id"]] = {
            "problem_id": p["id"], "verdict": verdict, "hint_used": hint_used
        }
    if st.button("送出並計分"):
        try:
            payload = {
                "user_id": st.session_state.assessment_user_id,
                "items": list(st.session_state.assessment_results.values())
            }
            resp = requests.post(f"{API_BASE}/assessment/score", json=payload, timeout=10)
            data = resp.json()
            st.success(f"建議等級:**{data['level']}**(score={data['score']})")
            with st.expander("查看每題計分"):
                for pid, info in data["breakdown"].items():
                    st.write(f"- #{pid} {info['title']} / {info['difficulty']} → "
                             f"{info['verdict']} / hint={info['hint_used']} / score={info['score']}")
        except Exception as e:
            st.error(f"計分失敗:{e}")
隨機抽出三題,一題Easy,一題Medinum,一題Hard:
做完後可選擇解題狀態(Accepted、WA、TLE...)
送出後可查看等級與每題計分。